/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.room.vo

import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.migration.bundle.FtsEntityBundle
import androidx.room.migration.bundle.TABLE_NAME_PLACEHOLDER
import androidx.room.parser.FtsVersion

/** An Entity with a mapping FTS table. */
class FtsEntity(
    element: XTypeElement,
    tableName: String,
    type: XType,
    properties: List<Property>,
    embeddedProperties: List<EmbeddedProperty>,
    primaryKey: PrimaryKey,
    constructor: Constructor?,
    shadowTableName: String?,
    val ftsVersion: FtsVersion,
    val ftsOptions: FtsOptions,
) :
    Entity(
        element,
        tableName,
        type,
        properties,
        embeddedProperties,
        primaryKey,
        emptyList(),
        emptyList(),
        constructor,
        shadowTableName,
    ) {

    override val createTableQuery by lazy { createTableQuery(tableName) }

    val nonHiddenProperties by lazy {
        properties.filterNot {
            // 'rowid' primary key column and language id column are hidden columns
            primaryKey.properties.isNotEmpty() && primaryKey.properties.first() == it ||
                ftsOptions.languageIdColumnName == it.columnName
        }
    }

    val contentSyncTriggerNames by lazy {
        if (ftsOptions.contentEntity != null) {
            arrayOf("UPDATE", "DELETE").map { operation ->
                createTriggerName(tableName, "BEFORE_$operation")
            } +
                arrayOf("UPDATE", "INSERT").map { operation ->
                    createTriggerName(tableName, "AFTER_$operation")
                }
        } else {
            emptyList()
        }
    }

    // Create trigger queries to keep fts table up to date with the content table as suggested in
    // https://www.sqlite.org/fts3.html#_external_content_fts4_tables_
    val contentSyncTriggerCreateQueries by lazy {
        if (ftsOptions.contentEntity != null) {
            createSyncTriggers(ftsOptions.contentEntity.tableName)
        } else {
            emptyList()
        }
    }

    override fun getIdKey(): String {
        val identityKey = SchemaIdentityKey()
        identityKey.append(tableName)
        identityKey.appendSorted(this@FtsEntity.properties)
        identityKey.append(ftsVersion.name)
        identityKey.append(ftsOptions)
        return identityKey.hash()
    }

    fun getCreateTableQueryWithoutTokenizer() = createTableQuery(tableName, false)

    private fun createTableQuery(tableName: String, includeTokenizer: Boolean = true): String {
        val definitions =
            nonHiddenProperties.map { it.databaseDefinition(false) } +
                ftsOptions.databaseDefinition(includeTokenizer)
        return "CREATE VIRTUAL TABLE IF NOT EXISTS `$tableName` " +
            "USING ${ftsVersion.name}(${definitions.joinToString(", ")})"
    }

    private fun createSyncTriggers(contentTable: String): List<String> {
        val contentColumnNames = nonHiddenProperties.map { it.columnName }
        return arrayOf("UPDATE", "DELETE").map { operation ->
            createBeforeTrigger(operation, tableName, contentTable)
        } +
            arrayOf("UPDATE", "INSERT").map { operation ->
                createAfterTrigger(operation, tableName, contentTable, contentColumnNames)
            }
    }

    private fun createBeforeTrigger(
        triggerOp: String,
        tableName: String,
        contentTableName: String,
    ) =
        "CREATE TRIGGER IF NOT EXISTS ${createTriggerName(tableName, "BEFORE_$triggerOp")} " +
            "BEFORE $triggerOp ON `$contentTableName` BEGIN " +
            "DELETE FROM `$tableName` WHERE `docid`=OLD.`rowid`; " +
            "END"

    private fun createAfterTrigger(
        triggerOp: String,
        tableName: String,
        contentTableName: String,
        columnNames: List<String>,
    ) =
        "CREATE TRIGGER IF NOT EXISTS ${createTriggerName(tableName, "AFTER_$triggerOp")} " +
            "AFTER $triggerOp ON `$contentTableName` BEGIN " +
            "INSERT INTO `$tableName`(" +
            (listOf("docid") + columnNames).joinToString(separator = ", ") { "`$it`" } +
            ") " +
            "VALUES (" +
            (listOf("rowid") + columnNames).joinToString(separator = ", ") { "NEW.`$it`" } +
            "); " +
            "END"

    // If trigger name prefix is changed be sure to update DBUtil#dropFtsSyncTriggers
    private fun createTriggerName(tableName: String, triggerOp: String) =
        "room_fts_content_sync_${tableName}_$triggerOp"

    override fun toBundle() =
        FtsEntityBundle(
            tableName,
            createTableQuery(TABLE_NAME_PLACEHOLDER),
            nonHiddenProperties.map { it.toBundle() },
            primaryKey.toBundle(),
            emptyList(),
            emptyList(),
            ftsVersion.name,
            ftsOptions.toBundle(),
            contentSyncTriggerCreateQueries,
        )
}
